<!DOCTYPE html>
<html>
<head>
  <title>GoJS Packed Class Hierarchy</title>
  <!-- Copyright 1998-2020 by Northwoods Software Corporation. -->
  <meta name="description" content="The JavaScript class hierarchy defined by the GoJS library, arranged in nested circles." />
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <script src="../assets/js/goSamples.js"></script>  <!-- this is only for the GoJS Samples framework -->
</head>
<body>
  <div id="sample">
    <div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:700px"></div>
    <p>
      Circle packing can be a useful way to visualize hierarchical data, as demonstrated here
      with a visualization of the class hierarchy of the GoJS library. This layout is performed
      automatically by the <a href="../extensions/PackedLayout.html">PackedLayout</a> extension. Nodes
      are sized according to how many properties their corresponding class has, or has inherited.
      As a result, larger nodes generally represent more complex classes. Mouse over nodes to see
      their full name and the number of properties on their corresponding class.
    </p>
    <p>
      This sample is very similar to the <a href="../samples/classHierarchy.html" target="_blank">Class Hierarchy</a> sample,
      except that instead of showing the class hierarchy as a tree, it is displayed using nested circles.
      Opening the API page is achieved by double-clicking on a node, rather than using a "HyperlinkText".
    </p>
  </div>

  <script type="module" id="code">
    import * as go from "../release/go-module.js";
    import { PackedLayout } from "./PackedLayout.js";

    if (window.goSamples) window.goSamples();  // init for these samples -- you don't need to call this
    const $ = go.GraphObject.make;  // for conciseness in defining templates

    // subclass PackedLayout to change default properties and override commitLayout function
    class HierarchyLayout extends PackedLayout {
      constructor() {
        super();
        this.packShape = PackedLayout.Spiral;
        this.hasCircularNodes = true;
        this.sortMode = PackedLayout.Area,
        this.comparer = function(na, nb) {
          // ensure label is placed last
          if (na.data.isLabel) {
            return 1;
          }
          if (nb.data.isLabel) {
            return -1;
          }
          // otherwise sort in ascending order by size (all nodes are circular, so using width or height here doesn't matter)
          return (na.actualBounds.width - nb.actualBounds.width);
        }
      }

      /* after each group has had its layout applied, size and position it according
      * to the smallest enclosing circle which goes around all of its nodes */
      commitLayout() {
        if (this.group !== null) {
          const groupData = keyToNodeDataMap.get(this.group.key);

          const enclosingCircle = this.enclosingCircle;
          const actualBounds = this.actualBounds;

          const size = new go.Size(enclosingCircle.width, enclosingCircle.width);
          this.diagram.model.setDataProperty(groupData, "size", size);

          const dx = enclosingCircle.centerX - actualBounds.centerX;
          const dy = enclosingCircle.centerY - actualBounds.centerY;
          const position = new go.Point((actualBounds.width - enclosingCircle.width) / 2 + dx, (actualBounds.height - enclosingCircle.height) / 2 + dy);
          this.diagram.model.setDataProperty(groupData, "position", position);
        }
      }
    }  // end HierarchyLayout


    const myDiagram =
      $(go.Diagram, "myDiagramDiv",  // must be the ID or reference to div
        {
          layout: $(HierarchyLayout), // defined above
          "animationManager.isEnabled": false,
          isReadOnly: true,
          initialAutoScale: go.Diagram.Uniform
        });

    // common definitions for both Nodes and Groups
    const toolTipTemplate =
      $(go.Adornment, "Auto",
        $(go.Shape, { fill: "white" }),
        $(go.TextBlock, { margin: 4 },
          new go.Binding("text", "tooltip"))
      );

    const selectionAdornmentTemplate =
      $(go.Adornment, "Auto",
        $(go.Shape, "Circle",
          { fill: null, stroke: "dodgerblue", strokeWidth: 3,
            spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight }),
        $(go.Placeholder)
      );

    function commonStyle() {
      return [
        {
          toolTip: toolTipTemplate,
          selectionAdornmentTemplate: selectionAdornmentTemplate,
          doubleClick: function(e, node) {
            const url = "../../api/symbols/" + node.data.key + ".html";
            window.open(url, "_blank");
          }
        }
      ];
    }

    myDiagram.nodeTemplate =
      $(go.Node, "Auto", commonStyle(),
        new go.Binding("width", "diameter"),
        new go.Binding("height", "diameter"),
        $(go.Shape,
          { figure: "Circle", fill: "#1F4963", strokeWidth: 0, spot1: go.Spot.TopLeft, spot2: go.Spot.BottomRight },
          new go.Binding("fill")),
        $(go.TextBlock,
          { font: "12px Helvetica, Arial, sans-serif", stroke: "white", maxLines: 1 },
          new go.Binding("text"),
          new go.Binding("font"),
          new go.Binding("stroke", "fill", function(f) { return go.Brush.isDark(f) ? "white" : "black"; }))
      );

    myDiagram.groupTemplate =
      $(go.Group, commonStyle(),
        { layout: $(HierarchyLayout) }, // defined above
        $(go.Shape, "Circle",
          { fill: "rgba(128,128,128,0.33)" },
          new go.Binding("desiredSize", "size"),
          new go.Binding("position")),
        $(go.Placeholder)  // represents area for all member parts
      );

    // Collect all of the data for the model of the class hierarchy
    const nodeDataArray = [
      { key: "GoJS", text: "GoJS", children: [] },
      // large label to be placed at the very end
      {
        key: "GoJS label",
        group: "GoJS",
        text: "GoJS",
        tooltip: "GoJS",
        fill: null,
        font: "64px Helvetica, bold Arial, sans-serif",
        isLabel: true,
        children: []
      }
    ];

    const keyToNodeDataMap = new go.Map();
    keyToNodeDataMap.add("GoJS", nodeDataArray[0]);

    // Iterate over all of the classes in "go"
    for (let k in go) {
      const cls = go[k];
      if (!cls) continue;
      const proto = cls.prototype;
      if (!proto) continue;
      proto.constructor.className = k;  // remember name

      let propCount = 0;  // count number of properties on the class
      for (let prop in proto) {
        if (proto.hasOwnProperty(prop)) {
          propCount++;
        }
      }

      const data = { key: k, text: k, propCount: propCount, children: [] };
      if (keyToNodeDataMap.has(k)) {
        data.children = keyToNodeDataMap.get(k).children;
      }
      keyToNodeDataMap.add(k, data); // will replace existing key if there is one

      // find base class constructor
      const base = Object.getPrototypeOf(proto).constructor;
      if (base === Object) {  // "root" node?
        data.group = "GoJS";
        keyToNodeDataMap.get("GoJS").children.push(data);
        nodeDataArray.push(data);
      } else {
        // add a node for this class and set its group to its parent
        data.group = base.className;
        if (keyToNodeDataMap.has(base.className)) {
          keyToNodeDataMap.get(base.className).children.push(data);
        } else {
          keyToNodeDataMap.add(base.className, {children: [data]});
        }

        nodeDataArray.push(data);
      }
    }

    // create groups and add labels to groups with only 1 child
    for (let i = nodeDataArray.length - 1; i >= 0; i--) {
      const n = nodeDataArray[i];
      if (n.children.length > 0) {
        n.isGroup = true;
      }

      // add tooltip and/or size node using the total number of properties it has and has inherited
      let totalCount = n.propCount;
      let parentKey = n.group;
      let parentData;
      while ((parentData = keyToNodeDataMap.get(parentKey)) !== null && parentData.propCount !== undefined) {
        totalCount += parentData.propCount;
        parentKey = parentData.group;
      }

      if (totalCount === undefined) { // applies to the root GoJS group only
        n.tooltip = n.text;
      } else {
        n.tooltip = n.text + ": " + totalCount; // add tooltip
      }
      if (!n.isGroup && !n.isLabel) { // only set size of node if it is not a group
        // calculate size by scaling totalCount logarithmically to produce more visually appealing results
        n.diameter = 20 * Math.log(0.5 * (totalCount + 5.5));
      }

      // add label to groups that only have one child
      if (n.children.length === 1) {
        nodeDataArray.push({
          text: n.text,
          tooltip: n.tooltip,
          group: n.key,
          fill: null,
          font: "20px Helvetica, bold Arial, sans-serif"
        });
      }

      delete n.children;
    }

    myDiagram.model = new go.GraphLinksModel(nodeDataArray);
  </script>
</body>
</html>